/*
 * Copyright 2015 Google Inc. All Rights Reserved.
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.google.blockly.ohos.ui;

import com.google.blockly.ohos.ZoomBehavior;
import com.google.blockly.ohos.detector.ScaleGestureDetector;
import com.google.blockly.utils.LogUtils;
import ohos.agp.components.AttrSet;
import ohos.agp.components.Component;
import ohos.agp.components.ComponentContainer;
import ohos.agp.render.Canvas;
import ohos.agp.render.Paint;
import ohos.agp.utils.Color;
import ohos.agp.utils.Rect;
import ohos.app.Context;
import ohos.multimodalinput.event.TouchEvent;

/**
 * Virtual view of a {@link WorkspaceView}.
 * <p/>
 * This view class provides a viewport-sized window into a larger workspace and supports panning and
 * zoom.
 */
public class VirtualWorkspaceView extends NonPropagatingViewGroup implements ComponentContainer.DrawTask, ComponentContainer.ArrangeListener,
        Component.EstimateSizeListener, Component.TouchEventListener {
    private static final String TAG = "VirtualWorkspaceView";
    private static final boolean DEBUG = false;

    // TODO(#87): Replace with configuration. Use dp.
    // Default desired width of the view in pixels.
    private static final int DESIRED_WIDTH = 2048;
    // Default desired height of the view in pixels.
    private static final int DESIRED_HEIGHT = 2048;

    private static final float MIN_SCALE_TO_DRAW_GRID = 0.5f;

    // Allowed zoom scales.
    private static final float[] ZOOM_SCALES = new float[]{0.25f, 0.5f, 1.0f, 2.0f};
    private static final int INIT_ZOOM_SCALES_INDEX = 2;

    protected boolean mScrollable = true;
    protected boolean mScalable = true;

    private final ViewPoint mPanningStart = new ViewPoint();

    private int INVALID_POINTER_ID = -1;

    // Fields for workspace panning.
    private int mPanningPointerId = INVALID_POINTER_ID;
    // Coordinates at the beginning of scrolling the workspace.
    private int mOriginalScrollX;
    private int mOriginalScrollY;
    // Scale and zoom in/out factor.
    private int mCurrentZoomScaleIndex = INIT_ZOOM_SCALES_INDEX;
    private float mViewScale = ZOOM_SCALES[INIT_ZOOM_SCALES_INDEX];

    private boolean mDrawGrid = true;

    // The workspace view that backs this virtual view.
    private WorkspaceView mWorkspaceView;
    // Flag indicating whether view should be reset before redrawing. This is set upon construction
    // to force an initial reset in the first call to onLayout. Call postResetView() to set this.
    private boolean mResetViewPending = true;

    private ScaleGestureDetector mScaleGestureDetector;

    private Rect mTempRect = new Rect();  // Used when calculating view-scaled block bounds.

    private Paint paint;
    private int mRadius = WorkspaceGridRenderer.DEFAULT_GRID_RADIUS;
    private int mGridSpacing = WorkspaceGridRenderer.DEFAULT_GRID_SPACING;

    public VirtualWorkspaceView(Context context) {
        this(context, null);
    }

    public VirtualWorkspaceView(Context context, AttrSet attrs) {
        super(context, attrs);
        addDrawTask(this, BETWEEN_BACKGROUND_AND_CONTENT);
        setArrangeListener(this);
        setEstimateSizeListener(this);
        setTouchEventListener(this);

        enableScrollBar(AXIS_X, mScrollable);
        enableScrollBar(AXIS_Y, mScrollable);
        mScaleGestureDetector = new ScaleGestureDetector(getContext(), new ScaleGestureListener());

        paint = new Paint();
        paint.setColor(Color.GRAY);
        paint.setStrokeWidth(mRadius);
    }

    public void setWorkspaceView(WorkspaceView workspaceView) {
        mWorkspaceView = workspaceView;
        // Setting the child view's pivot point to (0,0) means scaling leaves top-left corner in
        // place means there is no need to adjust view translation.
        mWorkspaceView.setPivotX(0);
        mWorkspaceView.setPivotY(0);
    }

    /**
     * Post a view reset to occur upon next call to {link onArrange(boolean, int, int, int, int)}.
     * <p/>
     * This method can be called when workspace content has been completely replaced, to provide the
     * user with a good initial view of the new workspace content.
     */
    public void postResetView() {
        mResetViewPending = true;
    }

    /**
     * Reset the view to the top-left corner of the virtual workspace (with a small margin), and
     * reset zoom to unit scale.
     * <p/>
     * This is called when the "reset view" button is clicked, or when
     * {@link #mResetViewPending} is set.
     */
    public void resetView() {
        // Reset scrolling state.
        mPanningPointerId = INVALID_POINTER_ID;
        mPanningStart.modify(0, 0);
        mOriginalScrollX = 0;
        mOriginalScrollY = 0;

        updateScaleStep(INIT_ZOOM_SCALES_INDEX);

        final Rect blocksBoundingBox = getViewScaledBlockBounds();
        final boolean useRtl = mWorkspaceView.getWorkspaceHelper().useRtl();
        if (mScrollable) {
            final int margin = mGridSpacing / 2;
            final int scrollToY = blocksBoundingBox.top - margin;
            if (useRtl) {
                scrollTo(blocksBoundingBox.right - getEstimatedWidth() + margin, scrollToY);
            } else {
                scrollTo(blocksBoundingBox.left - margin, scrollToY);
            }
        } else {
            // Reset top leading corner to 0,0 when
            scrollTo(useRtl ? -getWidth() : 0, 0);
        }
    }

    public boolean isScrollable() {
        return mScrollable;
    }

    public void setZoomBehavior(ZoomBehavior zoomBehavior) {
        setScrollable(zoomBehavior.isScrollEnabled());
        setScalable(zoomBehavior.isPinchZoomEnabled());
    }

    /**
     * Configures whether the user can scroll the workspace by dragging.  If scrolling is disabled,
     * the workspace will reset to 0,0 in the top right hand corner.
     *
     * @param scrollable Allow scrolling if true. Otherwise, disable it.
     */
    protected void setScrollable(boolean scrollable) {
        if (scrollable == mScrollable) {
            return;
        }
        mScrollable = scrollable;
        enableScrollBar(0, mScrollable);
        enableScrollBar(1, mScrollable);
        if (!mScrollable) {
            resetView();
        }
    }

    /**
     * Configures whether the user can scale the workspace by touch events.
     *
     * @param scalable Allow scalability if true. Otherwise, disable it.
     */
    protected void setScalable(boolean scalable) {
        if (mScalable == scalable) {
            return;
        }
        mScalable = scalable;
        if (!scalable) {
            resetView();
        }
    }

    /**
     * Zoom into (i.e., enlarge) the workspace.
     *
     * @return True if a zoom was changed, increased. Otherwise false.
     */
    public boolean zoomIn() {
        if (mCurrentZoomScaleIndex < ZOOM_SCALES.length - 1) {
            updateScaleStep(mCurrentZoomScaleIndex + 1);
            return true;
        }
        return false;
    }

    /**
     * Zoom out from (i.e.,shrink) the workspace.
     *
     * @return True if a zoom was changed, decreased. Otherwise false.
     */
    public boolean zoomOut() {
        if (mScrollable && mCurrentZoomScaleIndex > 0) {
            updateScaleStep(mCurrentZoomScaleIndex - 1);
            return true;
        }
        return false;
    }

    /**
     * @return The current view scale factor. Larger than 1.0 means zoomed in, smaller than 1.0
     * means zoomed out.
     */
    public float getViewScale() {
        return mViewScale;
    }

    int offsetX = 0;
    int offsetY = 0;

    int downX;
    int downY;

    @Override
    public boolean onTouchEvent(Component component, TouchEvent event) {
        if (mScalable && mScaleGestureDetector != null) {
            mScaleGestureDetector.onTouchEvent(event);
            if (mScaleGestureDetector.isInProgress()) {
                // If the scale gesture detector is handling a scale-and-pan gesture, then exit here
                // since otherwise we would also be generating dragging events below.
                return true;
            }
        }

        final int action = event.getAction();
        int pointerIdxx = -1;

        switch (action) {
            case TouchEvent.PRIMARY_POINT_DOWN: {
                clearFocus();
                if (mScrollable) {
                    pointerIdxx = event.getIndex();
                    mPanningPointerId = event.getPointerId(pointerIdxx);
                    downX = (int) event.getPointerPosition(pointerIdxx).getX();
                    downY = (int) event.getPointerPosition(pointerIdxx).getY();
                    mPanningStart.modify(downX, downY);

                    mOriginalScrollX = offsetX;
                    mOriginalScrollY = offsetY;
                }
                return true;
            }
            case TouchEvent.POINT_MOVE: {
                if (mPanningPointerId != INVALID_POINTER_ID) {
                    pointerIdxx = 0;
                    if (pointerIdxx == -1) {
                        // TODO: (#319) remove when we clean up multi-touch handling.
                        LogUtils.w(TAG, "Got an invalid pointer idx for the panning pointer.");
                        return false;
                    }

                    int x = (int) ((int) event.getPointerPosition(pointerIdxx).getX() + mOriginalScrollX - mPanningStart.getPointX());
                    int y = (int) ((int) event.getPointerPosition(pointerIdxx).getY() + mOriginalScrollY - mPanningStart.getPointY());
                    scrollTo(x, y);

                    return true;
                } else {
                    return false;
                }
            }
            case TouchEvent.OTHER_POINT_UP: {
                // Some pointer went up - check whether it was the one used for panning.
                final int pointerIdx = event.getIndex();
                final int pointerId = event.getPointerId(pointerIdx);
                if (pointerId != mPanningPointerId) {
                    return false;
                }
                // Pointer that went up was used for panning - treat like ACTION_UP.
                // FALLTHROUGH INTENDED.
            }
            case TouchEvent.PRIMARY_POINT_UP: {
                if (mPanningPointerId != INVALID_POINTER_ID) {
                    mPanningPointerId = INVALID_POINTER_ID;
                    return true;
                } else {
                    return false;
                }
            }
            case TouchEvent.CANCEL: {
                if (mPanningPointerId != INVALID_POINTER_ID) {
                    // When cancelled, reset to original scroll position.
                    scrollTo(mOriginalScrollX, mOriginalScrollY);
                    mPanningPointerId = INVALID_POINTER_ID;
                    return true;
                } else {
                    return false;
                }
            }
            default: {
                break;
            }
        }

        return true;
    }

    @Override
    public boolean onEstimateSize(int widthMeasureSpec, int heightMeasureSpec) {
        setEstimatedSize(
                getMeasuredSize(widthMeasureSpec, DESIRED_WIDTH),
                getMeasuredSize(heightMeasureSpec, DESIRED_HEIGHT));

        mWorkspaceView.estimateSize(
                EstimateSpec.getSizeWithMode(
                        (int) (getEstimatedWidth() / mViewScale), EstimateSpec.PRECISE),
                EstimateSpec.getSizeWithMode(
                        (int) (getEstimatedHeight() / mViewScale), EstimateSpec.PRECISE));
        return true;
    }

    @Override
    public boolean onArrange(int i, int i1, int i2, int i3) {
        if (mResetViewPending) {
            resetView();
            mResetViewPending = false;
        }

        // Shift the wrapped view's position to follow scrolling along. The scrolling of view
        // content is controlled by setTranslationX() and setTranslationY() in this.scrollTo()
        // below.
        final int offsetXX = offsetX;
        final int offsetYY = offsetY;
//        mWorkspaceView.arrange(offsetXX, offsetYY,
//                (int) ((getEstimatedWidth() / mViewScale) + offsetXX),
//                (int) ((getEstimatedHeight() / mViewScale) + offsetYY));
        return false;
    }

    @Override
    public void onDraw(Component component, Canvas canvas) {
        if (shouldDrawGrid()) {
            int startX = offsetX;
            int startY = offsetY;
            if (offsetX > 0) {
                startX = offsetX % (mRadius + mGridSpacing) - (mRadius + mGridSpacing);
            }
            if (offsetY > 0) {
                startY = offsetY % (mRadius + mGridSpacing) - (mRadius + mGridSpacing);
            }
            for (int i = startX; i < component.getWidth(); i += mGridSpacing + mRadius) {
                for (int j = startY; j < component.getHeight(); j += mGridSpacing + mRadius) {
                    canvas.drawCircle(i, j, mRadius, paint);
                }
            }
        }
    }

    /**
     * Set scroll position for the {@link WorkspaceView} wrapped by this instance.
     * <p/>
     * The given scroll position specifies the absolute offset of the displayed area within the
     * virtual workspace. Inside this method, the given scroll coordinates are clamped to their
     * valid ranges determined from the bounding box of all blocks in the virtual workspace, and
     * allowing for overscroll of half the width (or height, respectively) of this view.
     *
     * @param x The horizontal scroll position in pixel units of this view.
     * @param y The vertical scroll position in pixel units of this view.
     */
    @Override
    public void scrollTo(int x, int y) {
        if (!mScrollable) {
            return;
        }

        for (int i = 0; i < mWorkspaceView.getChildCount(); i++) {
            Component child = mWorkspaceView.getComponentAt(i);
            Rect rect = child.getComponentPosition();
            rect.offset(x - offsetX, y - offsetY);
            if (rect.left < 0 || rect.top < 0 || rect.right > getWidth() / mViewScale || rect.bottom > getHeight() / mViewScale) {
                return;
            }
        }

        offsetX = x;
        offsetY = y;

        mWorkspaceView.getWorkspaceHelper()
                .setVirtualWorkspaceViewOffset(
                        (int) (-x / mViewScale), /* virtual coords. */
                        (int) (-y / mViewScale));
        mWorkspaceView.postLayout();
    }

    /**
     * Get size for one dimension (width or height) of the view based on measure spec and desired
     * size.
     *
     * @param measureSpec The measure spec provided to {@link #onEstimateSize(int, int)} by its caller.
     * @param desiredSize The intrinsic desired size for this view.
     * @return The determined size, given measure spec and desired size.
     */
    private static int getMeasuredSize(int measureSpec, int desiredSize) {
        int mode = EstimateSpec.getMode(measureSpec);
        int size = EstimateSpec.getSize(measureSpec);

        if (mode == EstimateSpec.PRECISE) {
            return size;
        } else if (mode == EstimateSpec.UNCONSTRAINT) {
            return Math.min(size, desiredSize);
        } else {
            return desiredSize;
        }
    }

    private void updateScaleStep(int newScaleIndex) {
        if (newScaleIndex != mCurrentZoomScaleIndex) {
            final float oldViewScale = mViewScale;

            mCurrentZoomScaleIndex = newScaleIndex;
            mViewScale = ZOOM_SCALES[mCurrentZoomScaleIndex];

            // Add offset to current scroll coordinates so the center of the visible workspace area
            // remains in the same place.
            final float scaleDifference = mViewScale - oldViewScale;
            scrollBy((int) (scaleDifference * getEstimatedWidth() / 2),
                    (int) (scaleDifference * getEstimatedHeight() / 2));

            if (shouldDrawGrid()) {
                mRadius = (int) (WorkspaceGridRenderer.DEFAULT_GRID_RADIUS * mViewScale);
                mGridSpacing = (int) (WorkspaceGridRenderer.DEFAULT_GRID_SPACING * mViewScale);
            }

//            for (int i = 0; i < mWorkspaceView.getChildCount(); i++) {
//                Component child = mWorkspaceView.getComponentAt(i);
//                child.setScaleX(mViewScale);
//                child.setScaleY(mViewScale);
//            }
            mWorkspaceView.setScaleX(mViewScale);
            mWorkspaceView.setScaleY(mViewScale);

            invalidate();
        }
    }

    private boolean shouldDrawGrid() {
        return mDrawGrid && mViewScale >= MIN_SCALE_TO_DRAW_GRID;
    }

    private static int clampToRange(int y, int min, int max) {
        return Math.min(max, Math.max(min, y));
    }

    public int getGridSpacing() {
        return mGridSpacing;
    }

    public void setGridSpacing(int gridSpacing) {
        mGridSpacing = gridSpacing;
    }

    /**
     * EventsCallback class for scaling and panning the view using pinch-to-zoom gestures.
     */
    private class ScaleGestureListener extends ScaleGestureDetector.SimpleOnScaleGestureListener {
        // Focus point at the start of the pinch gesture. This is used for computing proper scroll
        // offsets during scaling, as well as for simultaneous panning.
        private float mStartFocusX;
        private float mStartFocusY;
        // View scale at the beginning of the gesture. This is used for computing proper scroll
        // offsets during scaling.
        private float mStartScale;
        // View scroll offsets at the beginning of the gesture. These provide the reference point
        // for adjusting scroll in response to scaling and panning.
        private int mStartScrollX;
        private int mStartScrollY;

        @Override
        public boolean onScaleBegin(ScaleGestureDetector detector) {
            mStartFocusX = detector.getFocusX();
            mStartFocusY = detector.getFocusY();

            mStartScrollX = getScrollValue(0);
            mStartScrollY = getScrollValue(1);

            mStartScale = mViewScale;
            return true;
        }

        @Override
        public boolean onScale(ScaleGestureDetector detector) {
            final float scaleFactor = detector.getScaleFactor();
            mViewScale *= scaleFactor;

            if (mViewScale < ZOOM_SCALES[0]) {
                mCurrentZoomScaleIndex = 0;
                mViewScale = ZOOM_SCALES[mCurrentZoomScaleIndex];
            } else if (mViewScale > ZOOM_SCALES[ZOOM_SCALES.length - 1]) {
                mCurrentZoomScaleIndex = ZOOM_SCALES.length - 1;
                mViewScale = ZOOM_SCALES[mCurrentZoomScaleIndex];
            } else {
                // find nearest zoom scale
                float minDist = Float.MAX_VALUE;
                // If we reach the end the last one was the closest
                int index = ZOOM_SCALES.length - 1;
                for (int i = 0; i < ZOOM_SCALES.length; i++) {
                    float dist = Math.abs(mViewScale - ZOOM_SCALES[i]);
                    if (dist < minDist) {
                        minDist = dist;
                    } else {
                        // When it starts increasing again we've found the closest
                        index = i - 1;
                        break;
                    }
                }
                mCurrentZoomScaleIndex = index;
            }

            if (shouldDrawGrid()) {
                mRadius = (int) (WorkspaceGridRenderer.DEFAULT_GRID_RADIUS * mViewScale);
                mGridSpacing = (int) (WorkspaceGridRenderer.DEFAULT_GRID_SPACING * mViewScale);
            }

//            for (int i = 0; i < mWorkspaceView.getChildCount(); i++) {
//                Component child = mWorkspaceView.getComponentAt(i);
//                child.setScaleX(mViewScale);
//                child.setScaleY(mViewScale);
//            }
            mWorkspaceView.setScaleX(mViewScale);
            mWorkspaceView.setScaleY(mViewScale);

            // Compute scroll offsets based on difference between original and new scaling factor
            // and the focus point where the gesture started. This makes sure that the scroll offset
            // is adjusted to keep the focus point in place on the screen unless there is also a
            // focus point shift (see next scroll component below).
            final float scaleDifference = mViewScale - mStartScale;
            final int scrollScaleX = (int) (scaleDifference * mStartFocusX);
            final int scrollScaleY = (int) (scaleDifference * mStartFocusY);

            // Compute scroll offset based on shift of the focus point. This makes sure the view
            // pans along with the focus.
            final int scrollPanX = (int) (mStartFocusX - detector.getFocusX());
            final int scrollPanY = (int) (mStartFocusY - detector.getFocusY());

            // Apply the computed scroll components for scale and panning relative to the scroll
            // coordinates at the beginning of the gesture.
            scrollTo(mStartScrollX + scrollScaleX + scrollPanX,
                    mStartScrollY + scrollScaleY + scrollPanY);

            mWorkspaceView.postLayout();
            return true;
        }
    }

    private Rect getViewScaledBlockBounds() {
        mWorkspaceView.getBlocksBoundingBox(mTempRect);
        mTempRect.left = (int) Math.floor(mTempRect.left * mViewScale);
        mTempRect.right = (int) Math.ceil(mTempRect.right * mViewScale);
        mTempRect.top = (int) Math.floor(mTempRect.top * mViewScale);
        mTempRect.bottom = (int) Math.ceil(mTempRect.bottom * mViewScale);
        return mTempRect;
    }
}
